package org.dei.perla.sys.device.adapter;

import java.util.ArrayList;

import org.dei.perla.sys.device.channel.Channel;
import org.dei.perla.sys.device.channel.ChannelManager;
import org.dei.perla.sys.device.channel.frame.address.FrameAddress;
import org.dei.perla.utils.logger.LogRecord.Type;
import org.dei.perla.utils.logger.LogRecord.Verbosity;
import org.dei.perla.utils.messages.AdapterChannelMessage;
import org.dei.perla.utils.messages.AdapterFpcMessage;
import org.dei.perla.utils.messages.Message;
import org.dei.perla.utils.messages.MsgUtility;
import org.dei.perla.utils.pipe.Pipe;

/**
 * Classe che gestisce un adapter server
 */

public class AdapterServerConcrete extends AdapterServer{
	
	/**
	 * Valore che definisce quando inizia il dominio dei VCI e finisce quello dei BVCI
	 */
	protected static final int bindDomain = 2000000000;
	
	/**
	 * Massima lunghezza della coda di una pipe
	 */
	protected static final int maxQueueLength = 100000;

    /**
     * Pipe sui quali vengono ricevuti dati dal Channel
     */
	private ArrayList<Pipe<AdapterChannelMessage>> pChannelPipeInList;
	/**
	 * Pipe sulla quale vengono scritti i dati per il Channel
	 */
	private ArrayList<Pipe<AdapterChannelMessage>> pChannelPipeOutList;
	
	
	/**
	 * Contatore progressivo delle pipe
	 * Serve solo per dare un nome univoco
	 */
	private int pPipeCounter;
	
	/**
	 * Pipe sui quali vengono ricevuti dati dall'FPC Factory
	 */
	private Pipe<AdapterFpcMessage> pFactPipeIn;
	/**
	 * Pipe sulla quale vengono scritti i dati per l'FPC Factory
	 */
	private Pipe<AdapterFpcMessage> pFactPipeOut;
	
	/**
	 * Costruttore 
	 * 
	 * @param parName, nome del componente
	 * @param parChannels, channel managers collegati al componente
	 */
    public AdapterServerConcrete(String parName, ArrayList<ChannelManager> parChannels) {
         
         super(parName, parChannels);
         
         this.pFactPipeIn = new Pipe<AdapterFpcMessage>(parName + "_rcv");
         this.pFactPipeOut = new Pipe<AdapterFpcMessage>(parName + "_snd");
         
    }
	
	/**
	 * Crea due nuove pipe di comunicazione da mandare all'FPC Factory per
	 * la creazione del nuovo FPC
	 * 
	 * @return le due nuove pipe create
	 */
	@SuppressWarnings("unchecked")
	private Pipe<Message>[] pCreateNewPipes(){
		
		Pipe<Message>[] tNewPipes = new Pipe[2];
		
		tNewPipes[0] = new Pipe<Message>("Pipe" + this.pPipeCounter, maxQueueLength);
		this.pPipeCounter++;
		tNewPipes[1] = new Pipe<Message>("Pipe" + this.pPipeCounter, maxQueueLength);
		this.pPipeCounter++;
		
		tNewPipes[0].start();
		tNewPipes[1].start();
		
		// Attacco la pipe di input al waiter
		this.pWaiter.addWaitable(tNewPipes[0].getWaitable());
		
		return tNewPipes;
	}
	
	@Override
	protected void mngAckMessage(Message parMsg) {
		
		AdapterFpcMessage tMsg = (AdapterFpcMessage) parMsg;
		int tBvci = MsgUtility.getBvci(tMsg.getPayload());
		
		FrameAddress tAddress = tMsg.getAddress();
		
		// Setto il nuovo vci
		int tVci = this.pReg.setNewVci(tBvci, tAddress);
		
		// creo un payload con il flag di ack
		byte[] tPayload = MsgUtility.createAckPayload(tBvci, tVci);
		
		// Creo un nuovo messaggio di ack
		AdapterChannelMessage tAckMsg = new AdapterChannelMessage(tAddress, tPayload);
	    
		// Trovo la pipe sulla quale scrivere dal registro
		Pipe<Message> tPipeOut = this.pReg.getLowerPipesFromVci(tVci)[1];
		
		// Se la pipe esiste
		if(tPipeOut != null){
			tPipeOut.enqueue(tAckMsg);
		}
		else{
			String tMessage = "Adapter Server: impossibile trovare pipe out per messaggio di binding";
			loggerNotify(Type.Error, tMessage, Verbosity.Medium);
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void mngBindingMsg(Message parMsg, Pipe<Message> parPipe) {

		int tBvci = MsgUtility.getBvci(parMsg.getPayload());
		
		FrameAddress tAddress = parMsg.getAddress();
		
		// Creo la tupla nella tabella di routing
		boolean ok = pBindNewDevice(parMsg, parPipe, this.pGetPipeOutFromPipeIn(parPipe));
		
		if(ok) {
			
			// Trovo le pipes del futuro fpc appena create
			Pipe[] tPipes = this.pReg.getUpperPipesFromBvciAndDeviceAddress(tBvci, tAddress);
			
			// Creo messaggio di binding
			AdapterFpcMessage tFrame = new AdapterFpcMessage(parMsg.getPayload(), tPipes[0], tPipes[1], MsgUtility.flagBinding, tAddress);	
			
			// Scrivo sulla pipe della factory
			this.pFactPipeOut.enqueue(tFrame);
			
		}
		
		// Se mi da errore la registrazione (componente già registrato)
		else{
			
			byte [] tNewPayload = MsgUtility.createNegativeAckPayload(tBvci);
			
			//..creo nuovo messaggio di ack negativo
			AdapterChannelMessage tMsg = new AdapterChannelMessage(tAddress, tNewPayload);
			
			// Non avendo registrato il componente uso le liste delle pipe
			// memorizzate nell'adapter server
			Pipe<Message> tPipeOut = this.pGetPipeOutFromPipeIn(parPipe);
			
			if(tPipeOut != null){
				tPipeOut.enqueue(tMsg);
			}
			else{
				String tMessage = "Adapter Server: impossibile trovare pipe out per messaggio di negative binding";
				loggerNotify(Type.Error, tMessage, Verbosity.Medium);
			}
			
		}
			
		
	}
	
	/**
	 * Crea le Pipe e le strutture dati per il nuovo
	 * dispotivo che si è collegato
	 * 
	 * @param parMsg, il messaggio ricevuto dal dispositivo
	 * @param parChannelPipeIn, la pipe di canale collegata al dispositivo (lettura)
	 * @param parChannelPipeOut, la pipe di canale collegata al dispositivo (scrittura)
	 */
	private boolean pBindNewDevice(Message parMsg, Pipe<Message> parChannelPipeIn, Pipe<Message> parChannelPipeOut){
		
		byte[] tPayload = parMsg.getPayload();
		
		int tBvci = MsgUtility.getBvci(tPayload);
		
		FrameAddress tAddress = parMsg.getAddress(); 
		
		// Crea le pipe
		Pipe<Message>[] tNewPipes;
		
		tNewPipes = this.pCreateNewPipes();
		
		// Memorizza BVCI e pipe nella struttura di gestione dei dati
		return this.pReg.addEntry(tBvci, tNewPipes[0], tNewPipes[1], parChannelPipeIn, parChannelPipeOut, tAddress);
		
	}

	@Override
	protected void mngNegativeAckMsg(Message parMsg) {
		
		AdapterFpcMessage tMsg = (AdapterFpcMessage) parMsg;
		byte[] tPayload = tMsg.getPayload();
		int tBvci = MsgUtility.getBvci(tPayload);
		FrameAddress tAddress = tMsg.getAddress();
		
		// Ricavo la pipe sulla quale mandare il mess di ack negativo
		Pipe<Message> tPipeOut = this.pReg.getLowerPipesFromBvciAndDeviceAddress(tBvci, tAddress)[1];
		
		byte [] tNewPayload = MsgUtility.createNegativeAckPayload(tBvci);
		
		AdapterChannelMessage tNewMsg = new AdapterChannelMessage(tAddress, tNewPayload);
		
		if(tPipeOut != null){
			
			// Mando il messaggio
			tPipeOut.enqueue(tNewMsg);
			
			// Rimuovo il componente prima registrato
			Pipe[] tPipes = this.pReg.getUpperPipesFromBvciAndDeviceAddress(tBvci, tAddress);
			tPipes[0].stop();
			tPipes[1].stop();
			
			this.pReg.removeEntryByBvciAndDeviceAddress(tBvci, tAddress);
			
		}
		else{
			String tMessage = "Adapter Server: impossibile trovare pipe out per messaggio di negative binding";
			loggerNotify(Type.Error, tMessage, Verbosity.Medium);
		}
		
	}

	@Override
	protected void mngNormalMsg(Message parMsg, Pipe parPipe) {
		
		byte[] tPayload = parMsg.getPayload();
		int tVci = MsgUtility.getVciNormalMessage(tPayload);
		
		FrameAddress tDeviceAddress = this.pReg.getDeviceAddressFromVci(tVci);
		
		Pipe<Message>[] tPipesOut = this.pReg.getPipesOutFromVci(tVci);
		Pipe<Message>[] tPipesIn  = this.pReg.getPipesInFromVci(tVci);
		// Se è un messagio in arrivo dagli fpc
		if(tPipesIn[0].equals(parPipe)){
			
			AdapterChannelMessage tMsg = new AdapterChannelMessage(tDeviceAddress, tPayload);
			tPipesOut[1].enqueue(tMsg);
			
		}
		// Se è un messaggio in arrivo dai canali
		else if(tPipesIn[1].equals(parPipe)){
			
			AdapterFpcMessage tMsg2 = new AdapterFpcMessage(parMsg.getAddress(), tPayload);
			tPipesOut[0].enqueue(tMsg2);
		
		}
		
		else{
			String tMessage = "Adapter Server: impossibile trovare pipe out per messaggio normale";
			loggerNotify(Type.Error, tMessage, Verbosity.Medium);
		}
	}
	
	/**
	 * Inizializza le pipe che collegano alla factory e 
	 * fa partire il componente
	 */
	public void start(){
		
		if(this.pChannelPipeInList.size() <= 0){
			
			String tMessage = "Adapter Server: impossibile trovare canali";
			loggerNotify(Type.Error, tMessage, Verbosity.Low);
			System.exit(0);
			
		}
		
//		 Avvia la pipe con la factory di input 
        this.pFactPipeIn.loggerDetachAll();
        this.pFactPipeIn.loggerAttach(this);
        this.pFactPipeIn.start();
        
        // Avvia la pipe con la factory di output 
        this.pFactPipeOut.loggerDetachAll();
        this.pFactPipeOut.loggerAttach(this);
        this.pFactPipeOut.start();
        
        this.pWaiter.addWaitable(this.pFactPipeIn.getWaitable());

		
		super.start();
		
	}
	
	/**
	 * Ferma il componente
	 */
	public void stop(){
		
		for (int i = 0; i < this.pChannelPipeInList.size(); i++) {
			
			this.pChannelPipeInList.get(i).stop();
			this.pChannelPipeOutList.get(i).stop();
		
		}
		
		this.pFactPipeIn.stop();
		this.pFactPipeOut.stop();
		
		super.stop();
	}

	/**
	 * Collega un nuovo canale all'Adapter server
	 * 
	 * @param c canale da aggiungere
	 */
	public void addNewChannel(Channel c){
		
		this.pChannelPipeInList.add(c.getInputPipe());
		this.pChannelPipeOutList.add(c.getOutputPipe());
		
		this.pWaiter.addWaitable(c.getInputPipe().getWaitable());
		
	}

	@Override
	protected void initialize() {
		
		this.pChannelPipeInList = new ArrayList<Pipe<AdapterChannelMessage>>();
		this.pChannelPipeOutList = new ArrayList<Pipe<AdapterChannelMessage>>();
		this.pPipeCounter = 0;
		this.pChannelIdCounter = bindDomain + 1;
		
		for(int i = 0; i < this.pChannelsMng.size(); i++){
			
			this.pChannelPipeInList.add(this.pChannelsMng.get(i).getInputPipe());
			this.pChannelPipeOutList.add(this.pChannelsMng.get(i).getOutputPipe());
			
		}
		
	}
	
	/**
	 * Ritorna la corrispondente pipe di canale di uscita data quella di entrata
	 * 
	 * @param pipeIn, la pipe di entrata
	 * @return la pipe di uscita corrispondente a quella di entrata
	 */
	private Pipe pGetPipeOutFromPipeIn(Pipe parPipeIn) {
		
		for (int i = 0; i < this.pChannelPipeOutList.size(); i++) {
			
			if (this.pChannelPipeInList.get(i).equals(parPipeIn)) {
				return this.pChannelPipeOutList.get(i);
			}
			
		}
		
		return null;
	}

	public Pipe<AdapterFpcMessage> getFactPipeIn() {
		return pFactPipeIn;
	}

	public Pipe<AdapterFpcMessage> getFactPipeOut() {
		return pFactPipeOut;
	}
	

}